
import java.awt.Color;
import java.awt.Graphics2D;
import java.util.ArrayList;

import org.jbox2d.collision.AABB;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.joints.DistanceJoint;
import org.jbox2d.dynamics.joints.MouseJoint;
import org.jbox2d.dynamics.joints.RevoluteJoint;

import pulpcore.CoreSystem;
import pulpcore.Stage;
import pulpcore.animation.Easing;
import pulpcore.animation.Fixed;
import pulpcore.animation.Timeline;
import pulpcore.animation.event.RemoveSpriteEvent;
import pulpcore.image.BlendMode;
import pulpcore.image.CoreFont;
import pulpcore.image.CoreGraphics;
import pulpcore.image.CoreImage;
import pulpcore.math.CoreMath;
import pulpcore.Input;
import pulpcore.scene.Scene2D;
import pulpcore.sound.Sound;
import pulpcore.sprite.Button;
import pulpcore.sprite.Group;
import pulpcore.sprite.ImageSprite;
import pulpcore.sprite.Label;
import pulpcore.sprite.Sprite;
import pulpfizz.physics.Actor;
import pulpfizz.physics.BodyUtils;
import pulpfizz.physics.JointUtils;
import pulpfizz.physics.Material;
import pulpfizz.physics.NewWorld;
import pulpfizz.physics.PhysicsTileMap;
import pulpfizz.physics.contact.ContactData;
import pulpfizz.physics.contact.ContactEventListener;
import pulpfizz.pulp.CameraControlledScene;
import pulpfizz.pulp.CameraSprite;
import pulpfizz.pulp.body.BodySprite;
import pulpfizz.pulp.body.ImageBodySprite;
import pulpfizz.pulp.body.Java2DBodySprite;
import pulpfizz.pulp.body.PhysicsLayer;
import pulpfizz.pulp.body.SimpleBodyDrawer;
import pulpfizz.pulp.ui.LabelBox;
import pulpfizz.pulp.ui.SmoothMouse;

/**
 * A heavily detailed demonstration of the
 * contact handling features within PulpFizz.
 * 
 * Much of this code is the same as BasicDemo; where that
 * is the case, comments are mostly omitted.
 * 
 * @author eric
 *
 */
public class ContactHandlingDemo extends CameraControlledScene
{

	private PhysicsLayer physics;
	private SmoothMouse sm;
	private LabelBox fpsSprite;
	
	private Button resetButton;
	
	/*
	 * These are some groups that our objects will be members of.
	 * Group numbers must be in the range 0-31; things will go 
	 * wrong if you don't abide by this rule!
	 */
	static final int SCENERY_GROUP 	= 	0;
	static final int CIRCLE_GROUP 	= 	1;
	static final int BOX_GROUP 		= 	2;
	static final int RED_GROUP 		= 	3;
	static final int GREEN_GROUP 	= 	4;
	static final int BLUE_GROUP 	= 	5;
	
	/* Frame rate counter variables. */
	int timeElapsed = 0, numFrames=0;
    
    @Override
	public void load()
	{
        super.load();
        Stage.setFrameRate(60);

		/* 
		 * PhysicsLayer setup - mostly boilerplate code.
		 * See BasicDemo for detailed comments.
		 */
		physics = new PhysicsLayer(20);
		physics.setAnchor(Sprite.CENTER);
		add(physics);
		physics.fitToStage();
        physics.setGravity(new Vec2(0,-10.0f));
        
        sm = new SmoothMouse();
		add(sm);
		
		fpsSprite = new LabelBox("",10,10);
		addToHUD(fpsSprite);
		
		resetButton = Button.createLabeledButton("Reset", Stage.getWidth()-10, 10);
        resetButton.setAnchor(Sprite.NORTH_EAST);
        addToHUD(resetButton);
        
        /* 
         * Now for something new: set up particle graphics.
         * This is straight from the PulpCore Particles example.
         */
        images = CoreImage.load("particles.png").split(6, 1);
        particleLayer = new Group();
        // Particles should ignore mouse input
        particleLayer.enabled.set(false);
        // Particles look good with additive blending
        particleLayer.setBlendMode(BlendMode.Add());
        addLayer(particleLayer);
        
		createWorld();
	}
    
	public void createWorld()
	{
		NewWorld world = physics.getWorld();

		/* See BasicDemo for explanation of border creation. */
		Body border = BodyUtils.createBoundingBox(world, 0.5f);
        Java2DBodySprite borderSprite = new Java2DBodySprite(border, physics);
		physics.add(borderSprite);
		
		/*
		 * An Actor is associated with a body (or group of bodies)
		 * and represents game meta-data.  To use the PulpFizz contact
		 * handling utilities you must associate an Actor with a body.
		 * You may wish to extend the Actor class to add more meta-data
		 * to your bodies, as well.
		 * 
		 * Simply pass the body to the Actor constructor,
		 * then we can set some Actor properties.
		 */
		Actor borderActor = new Actor(border);
		borderActor.setName("Border");
		borderActor.addToGroup(SCENERY_GROUP);
		
		/* 
		 * The next blocks of code just create a bunch
		 * of red, green, and blue circles and squares.
		 */
        AABB worldAABB = world.getWorldAABB();
        float minX = worldAABB.lowerBound.x + 1.0f;
        float widthX = worldAABB.upperBound.x - minX - 2.0f;
        float minY = worldAABB.lowerBound.y + 1.0f;
        float heightY = worldAABB.upperBound.y - minY - 2.0f;
        
        Actor firstActor = null;
        Actor lastActor = null;
        int nObjects = 12;
        for (int i=0; i<nObjects; ++i) {
        	float myX = minX + (float)(Math.random()*widthX);
        	float myY = minY + (float)(Math.random()*heightY);
        	float myRad = 0.5f;
        	
        	/* Keeping track of whether we're making a circle or a box. */
        	Body myBody = null;
        	boolean isCircle = true;
        	String typeStr = "";
        	int typeGroup = 0;
        	if (i%2 == 0) {
        		myBody = BodyUtils.createCircle(world, myX, myY, myRad);
        		typeStr = "circle";
        		typeGroup = CIRCLE_GROUP;
        	} else {
        		myBody = BodyUtils.createBox(world, myX, myY, myRad, myRad, 0f);
        		isCircle = false;
        		typeStr = "box";
        		typeGroup = BOX_GROUP;
        	}
        	
        	/* 
        	 * If you have no particular preference about the material
        	 * properties, Material.WOOD() is a pretty good default choice.
        	 */
        	Material.WOOD().applyAll(myBody); 
        	
        	/* Keeping track of what color object we're creating. */
        	int whatColor = i%3; //0=red, 1=green, 2=blue
        	String colorStr = "";
        	Color myColor = null;
        	int colorGroup = 0;
        	if (whatColor == 0) {
        		myColor = Color.RED;
        		colorStr = "Red";
        		colorGroup = RED_GROUP;
        	} else if (whatColor == 1) {
        		myColor = Color.GREEN;
        		colorStr = "Green";
        		colorGroup = GREEN_GROUP;
        	} else if (whatColor == 2) {
        		myColor = Color.BLUE;
        		colorStr = "Blue";
        		colorGroup = BLUE_GROUP;
        	}
        	
        	Java2DBodySprite mySprite = new Java2DBodySprite(myBody, physics, 3.0f, myColor, myColor);
    		physics.add(mySprite);
    		
    		/* 
    		 * Now for the important stuff, the Actor creation.
    		 * When we create a new Actor and pass a Body to its
    		 * constructor, the Actor is associated with the Body.
    		 * Many bodies may share the same Actor, but a body may
    		 * only have one Actor associated with it.  The Actor
    		 * is stored in the m_userData field of the body.
    		 * 
    		 * In JBox2D, userData is an Object, so in theory anything
			 * may be stored there; however, in PulpFizz it is 
			 * highly recommended that you never set the userData
			 * yourself, as this will ensure that it is always an Actor
			 * (as long as you create your bodies in a NewWorld, even
			 * if you don't create an Actor, a default (and shared)
			 * Actor with no interesting properties is attached to your
			 * body - this actor may be accessed as Actor.NULL_ACTOR, but
			 * be warned, it's not very useful, and is really only used
			 * to avoid casting exceptions!).
			 * 
			 * If you may need to identify your body by name or id, or
			 * add it to groups, or pretty much anything non-trivial,
			 * you should always manually create an Actor for it.  For
			 * real games you'll often want to subclass Actor and add
			 * your own meta-data and behaviors to the subclasses.
			 */
    		Actor myActor = new Actor(myBody);
    		
    		/* 
    		 * As we'll see later, using informative names for your
    		 * actors can be quite useful.
    		 */
    		myActor.setName(colorStr+" "+typeStr+" id #"+myActor.getID());
    		
    		/* 
    		 * We add each object to its type group and its color group.
    		 * An Actor may belong to as many groups as you wish, though
    		 * there is a maximum of 32 groups in total (int flags are used
    		 * to track group membership, and an int has 32 bits).
    		 * 
    		 * Note that these groups are totally separate from the Box2d
    		 * collision groups, which may be defined separately (and relate to whether
    		 * collisions happen at all, whereas these relate to the reporting
    		 * of collisions that occur).
    		 */
    		myActor.addToGroup(typeGroup);
    		myActor.addToGroup(colorGroup);
    		
    		if (i == 0) firstActor = myActor;
    		if (i == nObjects-1) lastActor = myActor;
        }
        
        /* 
         * We still aren't listening for collisions, so we have to create
         * and register listeners.  Here we'll use the same listener for
         * everything, which creates a burst of sprites and prints the
         * involved object names.  We'll define it inline for clarity, though
         * you'll usually prefer to define a separate class that implements
         * the ContactEventListener interface.
         */
        ContactEventListener anyOldListener = new ContactEventListener() {
			public void trigger(ContactData c) {
				/*
				 * There are ADD, REMOVE, and PERSIST events.
				 * Usually we're only interested in the ADD ones,
				 * so we discard the others.
				 */
				if (c.state != ContactData.ADD) return;
				
				/* 
				 * We retrieve the Actor objects from the body userData.
				 */
				Actor a1 = (Actor)(c.shape1.getBody().getUserData());
				Actor a2 = (Actor)(c.shape2.getBody().getUserData());
				
				/* 
				 * Now we're going to spawn some particles, using
				 * code from the PulpCore Particles example - these
				 * are pure PulpCore objects, so they world locations
				 * must be converted to canvas locations before use.
				 * 
				 * If you want to spawn physical particles, it's a bit
				 * more work...
				 */
				Vec2 collisionWorldPoint = c.position;
				Vec2 collisionCanvasPoint = physics.getCanvasVector(collisionWorldPoint);
				makeParticles((int)collisionCanvasPoint.x, (int)collisionCanvasPoint.y,
							  (int)collisionCanvasPoint.x, (int)collisionCanvasPoint.y,
							  3);
						  
				System.out.println(a1.getName() + " collided with " + a2.getName());
			}
        };
        
        /*
         * We've got a listener, but we still have to register it with the types
         * of things to listen for.  Try commenting/uncommenting some of these lines
         * to see the types of things you can listen for.
         */
        
        /* Any collision with the wall. */
        world.getContactEventDispatcher().registerGroupListener(SCENERY_GROUP, anyOldListener);
        
        /* 
         * Another way to listen for wall collisions (since the border is the
         * only current member of the SCENERY_GROUP this is equivalent to the
         * listener defined above). 
         */
        //world.getContactEventDispatcher().registerActorListener(borderActor.getID(), anyOldListener);

        /* Any collision involving the first body created. */
        //world.getContactEventDispatcher().registerActorListener(firstActor.getID(), anyOldListener);
        
        /* Only collisions between the first and last bodies created. */
        //world.getContactEventDispatcher().registerActorActorListener(firstActor.getID(), lastActor.getID(), anyOldListener);
        
        /* Any collision involving a red object. */
        //world.getContactEventDispatcher().registerGroupListener(RED_GROUP, anyOldListener);
        
        /*
         * Warning: group/group collision handling gets slow when objects are members
         * of many groups - the time taken to dispatch a collision event is O(N*M)
         * where N and M are the numbers of groups each object involved in a collision
         * belongs to.  In the worst case N and M could both be 32, which means a possible
         * 1024 listeners must be checked for.  Early exit optimizations are in the works,
         * but keeping the number of groups each object belongs to to a reasonable number
         * is the best solution - even if you don't have any listeners registered for many
         * pair of groups, lookups are required to determine that, which could get slow.
         */
        
        /* Any collision between a red object and a circle. */
        //world.getContactEventDispatcher().registerGroupGroupListener(CIRCLE_GROUP, RED_GROUP, anyOldListener);
        
        /* Any collision between two circles. */
        //world.getContactEventDispatcher().registerGroupGroupListener(CIRCLE_GROUP, CIRCLE_GROUP, anyOldListener);
        
        
	}
	
	   
    @Override
	public void update(int elapsedTime)
	{
    	/* 
    	 * Not actually necessary, as Scene2D::update does nothing...
    	 * Calling the superclass update method is good practice to
    	 * get into, though, and it won't hurt performance if it's
    	 * empty since the JVM will prune the call.
    	 */
		super.update(elapsedTime);
		
		if (resetButton.isClicked() || Input.isPressed(Input.KEY_R)) {
			physics.resetWorld();
			physics.removeAll();
			createWorld();
		}
		
        timeElapsed += elapsedTime;
        numFrames++;
        if(timeElapsed >= 1000) {
            timeElapsed = 0;
            fpsSprite.setText("FPS="+ numFrames);
            numFrames=0;
        }
        
        if (Input.isMousePressed() && physics != null) {
        	double pX = physics.getPhysicsX(sm.x.get(), sm.y.get());
			double pY = physics.getPhysicsY(sm.x.get(), sm.y.get());
			Vec2 mouseClickWorld = new Vec2( (float)pX, (float)pY );
			Body[] bList = BodyUtils.getAllBodiesInCircle(physics.getWorld(), mouseClickWorld, 2.0f);
			
			if (bList != null) {
				for (int i=0; i<bList.length; ++i) {
					Body b = bList[i];
					Vec2 imp = mouseClickWorld.sub(b.getWorldCenter());
					float dist = imp.normalize() + .1f;
					imp.mulLocal(-30.0f*b.getMass() / (dist*dist));
					b.applyImpulse(imp, b.getWorldCenter());
					
				}
			}
			
			/* 
			 * This time around, let's add some particles to our explosion!
			 * Notice that even though this addition happens in canvas-space,
			 * we don't use the raw mouse coordinates - due to the camera
			 * handling, the best approach is to first convert the mouse
			 * coordinates to world coordinates and then to canvas coordinates.
			 * 
			 * This may be simplified in a future version.
			 */
			Vec2 mouseCanvasPoint = physics.getCanvasVector(mouseClickWorld);
			makeParticles((int)mouseCanvasPoint.x, (int)mouseCanvasPoint.y,
						  (int)mouseCanvasPoint.x, (int)mouseCanvasPoint.y,
						  50);
			
		}

	}
    
    
    /*
     * The particle handling here is ripped almost directly from the PulpCore
     * Particles example, with some minor changes to sizes.  These particles
     * are not physical, they are just moving graphics.  The coordinates are 
     * canvas coordinates, not world coordinates.
     */
    
    private CoreImage[] images;
    private Group particleLayer;
    
    private void makeParticles(int x1, int y1, int x2, int y2, int numParticles) {
        
        Timeline timeline = new Timeline();
        
        for (int i = 0; i < numParticles; i++) {
            int size = CoreMath.rand(4, 24);
            int duration = (200 - size) * 3;
            int moveDistance = CoreMath.rand(4, 80 - size);
            double moveDirection = CoreMath.rand(0, 2*Math.PI);
            
            int startX = x1 + i * (x2 - x1) / numParticles;
            int startY = y1 + i * (y2 - y1) / numParticles;
            int goalX = startX + (int)(moveDistance * Math.cos(moveDirection));
            int goalY = startY + (int)(moveDistance * Math.sin(moveDirection));
            double startAngle = CoreMath.rand(0, 2*Math.PI);
            double endAngle = startAngle + CoreMath.rand(-2*Math.PI, 2*Math.PI);
            
            CoreImage image = images[CoreMath.rand(images.length - 1)];
            Sprite sprite = new ImageSprite(image, startX, startY);
            sprite.setAnchor(Sprite.CENTER);
            sprite.setSize(size, size);
            particleLayer.add(sprite);
            
            timeline.animateTo(sprite.x, goalX, duration, Easing.REGULAR_OUT);
            timeline.animateTo(sprite.y, goalY, duration, Easing.REGULAR_OUT);
            timeline.animate(sprite.angle, startAngle, endAngle, duration);
            timeline.at(100).animateTo(sprite.alpha, 0, duration - 100, Easing.REGULAR_OUT);
            timeline.add(new RemoveSpriteEvent(particleLayer, sprite, duration));
        }
        
        addTimeline(timeline);
    }

}